package com.sun.tools.xjc.addons.android_plugin;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlAccessType;

import org.xml.sax.ErrorHandler;

import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JAnnotationValue;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JCatchBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JEnumConstant;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JForEach;
import com.sun.codemodel.JForLoop;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JStatement;
import com.sun.codemodel.JStringLiteral;
import com.sun.codemodel.JTryBlock;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.EnumOutline;
import com.sun.tools.xjc.outline.Outline;

public class AndroidPlugin extends Plugin {
  //private JDefinedClass androidPluginClass;

  @Override
  public String getOptionName() {
    return "Xandroid";
  }

  @Override
  public String getUsage() {
    return "  -Xandroid          :  create classes with xml parsing suitable for android devices";
  }

  @Override
  public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) {
    // Process every pojo class generated by jaxb
    //androidPluginClass = createAndroidSerializableClass();
    
    //ClassOutline cOutline = new Cl
    
    for (ClassOutline classOutline : outline.getClasses()) {
      final JDefinedClass jClass = classOutline.implClass;
      
      processClass(jClass);

    }
    for(EnumOutline enumOutline : outline.getEnums()) {
      JDefinedClass jEnum = enumOutline.clazz;
      
      processEnum(jEnum);
    }
    
    //Boolean res = outline.getClasses().add(androidPluginClass);
    return true;
  }
  
  private JDefinedClass createAndroidSerializableClass() {
    JCodeModel model = new JCodeModel();
    JDefinedClass asClass = null;
    try {
      asClass = model._class("edxl.utilities.AndroidSerializableClass");
    } catch (JClassAlreadyExistsException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    
    return asClass;
  }
  
  private void processEnum(JDefinedClass enuum) {
    clearAllAnnotations(enuum);
  // TODO  
  }
  
  private boolean processClass(JDefinedClass clazz) {
    addSerialization(clazz);
    clearAllAnnotations(clazz);
    
    /*
    for(JClass nestedJClazz : clazz.listClasses()) {
      if(nestedJClazz instanceof JDefinedClass) {
        JDefinedClass nestedClazz = (JDefinedClass) nestedJClazz;
        
        processClass(nestedClazz);
      }
    }
    */
    
    return true;
  }
  
  private boolean addFromXml(JDefinedClass clazz) {
    // create the fromXml class
    String methodName = "fromXml";
    JMethod jmCreate = clazz.method(JMod.PUBLIC | JMod.STATIC, clazz, methodName);//clazz.elementType(), methodName);
    JBlock jBlock = jmCreate.body();
    
    // create the method parameter String xml
    jmCreate.param(java.lang.String.class, "xml");
    
    // SAMPLE CODE FOR THE MOMENT
    
    /* Defining some class Variable in method body */
    JClass jClassavpImpl = clazz;
    JVar jvarAvpImpl = jBlock.decl(jClassavpImpl, "newClass");
    jvarAvpImpl.init(JExpr._new(jClassavpImpl));
    /* Adding some direct statement */
    jBlock.directStatement("newClass.setCode(100);");
    /* returning variable */ 
    jBlock._return(jvarAvpImpl);
    
    return true;
  }
  

  private List<String> getOrderedElementList(
      JDefinedClass clazz)
  {
    
    Collection<JAnnotationUse> annotations = clazz.annotations();

    // go through all annotations until we find the XmlType annotation
    for(JAnnotationUse annotation : annotations) {
      JClass annotationClass = annotation.getAnnotationClass();
      String annotationName = annotationClass.name();
      if(annotationName.equals("XmlType")) {
        // once we've found the XmlType annotation, we look for the propOrder
        // member
        Map<String, JAnnotationValue> memberMap = annotation.getAnnotationMembers();
        JAnnotationArrayMember order = 
            (JAnnotationArrayMember) memberMap.get("propOrder");
        if(order != null) {
          // if the propOrder member exists, we extract out the elements defined
          // in that member.
          try{
          ArrayList<String> elementList = new ArrayList<String>();
          ArrayList<?> values = Utilities.getFieldValue(order, "values");
          for(Object value : values) {
            JStringLiteral jstringValue = Utilities.getFieldValue(value, "value");
            String strValue = jstringValue.str;
            
            elementList.add(strValue);
          }
          return elementList;
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }
    }
    return null;
  }
  
  Map<String, APXmlAttribute> getAttributeInformation(JDefinedClass clazz) {
    Map<String, APXmlAttribute> attributeMap = new HashMap<>();
    Map<String, JFieldVar> jfieldsMap = clazz.fields();
    for(Map.Entry<String, JFieldVar> entry : jfieldsMap.entrySet()) {
      String fieldName = entry.getKey();
      JFieldVar jfield = entry.getValue();
      Collection<JAnnotationUse> fieldAnnotations = jfield.annotations();
      
      // determine if the field represents an attribute
      for(JAnnotationUse fieldAnnotation : fieldAnnotations) {
        String name = fieldAnnotation.getAnnotationClass().name();
        if(name.equals("XmlAttribute")) {
          APXmlAttribute attribute;
          try {
            attribute = new APXmlAttribute(fieldAnnotation, fieldName, jfield);
            attributeMap.put(fieldName, attribute);
          } catch (Exception e) {
            // do nothing
          }
          break;
        }
      }
    }
    return attributeMap;
  }
  
  Map<String, APXmlElement> getElementInformation(JDefinedClass clazz) {
    // TODO this is only valid for XmlAccessType = FIELD for the class. Need to generalize at some point.

    Map<String, APXmlElement> elementMap = new HashMap<String, APXmlElement>();
    Map<String, JFieldVar> jfieldsMap = clazz.fields();
    for(Map.Entry<String, JFieldVar> entry : jfieldsMap.entrySet()) {
      String fieldName = entry.getKey();
      JFieldVar jfield = entry.getValue();
      Collection<JAnnotationUse> fieldAnnotations = jfield.annotations();
      
      // determine if the field represents an element
      Boolean found = false;
      for(JAnnotationUse fieldAnnotation : fieldAnnotations) {
        String name = fieldAnnotation.getAnnotationClass().name();
        if(name.equals("XmlElement") || 
            name.equals("XmlElements") || 
            name.equals("XmlElementRef") || 
            name.equals("XmlElementRefs")) {
          APXmlElement element;
          try {
            element = new APXmlElement(fieldAnnotation, fieldName, jfield);
            elementMap.put(fieldName, element);
          } catch (Exception e) {
            e.printStackTrace();
          }
          
          found = true;
          break;
        } else if (name.equals("XmlValue")) {
          APXmlElement element =new APXmlElement();
          element.variableName = fieldName;
          element.names.add("@XmlValue");
          element.field = jfield;
          element.types.add(null);
          element.namespaces.add(null);
          
          elementMap.put(fieldName, element);
          
          found = true;
          break;
        } else if (name.equals("XmlAttribute") || name.equals("XmlTransient")) {
          found = true; // not an element for sure, so we are considering this "found" so we don't deal with attributes below
        }
      }
      
      if(!found) {
        // because we're handeling classes with accessor type = field, everything
        // that's not an annotation or transient type is an element
        APXmlElement element = new APXmlElement();
        element.field = jfield;
        element.variableName = fieldName;
        element.names.add(fieldName);

        element.types.add(null);
        element.namespaces.add(null);

        elementMap.put(fieldName, element);
      }
    }
    return elementMap;
  }
  
  private void addAttributeInformation(JBlock jBlock, JVar stringBuilderVar, Map<String, APXmlAttribute> attributeInfos) {
    if(attributeInfos.size() > 0) {
      jBlock.directStatement("// adding attribute information");
      
      for(Map.Entry<String, APXmlAttribute> entry : attributeInfos.entrySet()) {
        APXmlAttribute attributeInfo = entry.getValue();
        
        JType type = attributeInfo.field.type();
        
        JBlock attrBlock = new JBlock();
        attrBlock.invoke(stringBuilderVar, "append").arg(" ");
        attrBlock.invoke(stringBuilderVar, "append").arg(attributeInfo.name);
        attrBlock.invoke(stringBuilderVar, "append").arg("=\"");
        attrBlock.invoke(stringBuilderVar, "append").arg(JExpr.ref(attributeInfo.variableName));
        attrBlock.invoke(stringBuilderVar, "append").arg("\"");

        if(type.isPrimitive()) {
          jBlock.add(attrBlock);
        } else {
          JConditional ifBlock = jBlock._if(JExpr.ref(attributeInfo.variableName).ne(JExpr._null()));
          ifBlock._then().add(attrBlock);
        }
        
        type = null;
      }
    }
  }
  
  private static int i = 0;
  private void addElementsInformation(
      JCodeModel jCodeModel,
      JBlock jBlock,
      JVar jStringBuilderVar,
      List<String> elementList,
      Map<String, APXmlElement> elementInfos) {
    
    System.out.println(i++);
    
    if(elementList != null && elementList.size() > 0) {
      for(String elementName : elementList) {
        APXmlElement elementInfo = elementInfos.get(elementName);
        Type innerType = null;

        Boolean isList = false;
        {
          try {
            Object basis = Utilities.getFieldValue(elementInfo.field.type(), "basis");
            Class<?> typeClass = Utilities.getFieldValue(basis, "_class");
            
            if(typeClass.equals(java.util.List.class)) {
              isList = true;
              
              try {
                innerType = typeClass.getGenericInterfaces()[0];
              } catch (Exception e) {
                // do nothing
              }
            }
          } catch (Exception e) {
            // do nothing
          }
        }
        
        JBlock codeBlock = jBlock;
        String varName = elementInfo.variableName;
        String xmlName = elementInfo.variableName;
        if(elementInfo.names.get(0) != null) {
          xmlName = elementInfo.names.get(0);
        }
        
        if(isList) {
          varName = "__item";
          JConditional ifStatement = jBlock._if(JExpr.ref(elementInfo.variableName).ne(JExpr._null()));
          JForEach foreachloop = ifStatement._then().forEach(jCodeModel._ref(Object.class), varName, JExpr.ref(elementInfo.variableName));
          codeBlock = foreachloop.body();
        }
        
        if(elementInfo != null) {
          if(elementInfo.names.size() == 1) {
            JType type = elementInfo.field.type();
            if(xmlName.equals("@XmlValue")) {
              codeBlock.directStatement(" ");
              codeBlock.directStatement("// adding the element value");
              codeBlock.invoke(jStringBuilderVar, "append").arg(JExpr.ref(varName));
            } else {
              codeBlock.directStatement(" ");
              codeBlock.directStatement("// adding the element for " + varName);

              // TODO trying to get the inner type of the list generic so I can
              // appropriately cast to that value.
              JBlock appendBlock = new JBlock();
              if(innerType != null) {
                JTryBlock tryBlock = appendBlock._try();
                
                JClass classClass = jCodeModel.ref(java.lang.Class.class);
                JVar classVar = tryBlock.body().decl(classClass, "__class");
                classVar.init(JExpr.invoke(JExpr.ref(varName), "getClass"));
                
                JClass methodClass = jCodeModel.ref(java.lang.reflect.Method.class);
                JVar methodVar = tryBlock.body().decl(methodClass, "__method");
                methodVar.init(JExpr.invoke(classVar, "getMethod").arg("toXml").arg(JExpr.direct("java.lang.String.class")));
                
                tryBlock.body().invoke(jStringBuilderVar, "append").arg(JExpr.invoke(methodVar, "invoke").arg(JExpr.ref(varName)).arg(xmlName));
                
                JCatchBlock catchBlock = tryBlock._catch(jCodeModel.ref(java.lang.Exception.class));
                catchBlock.body().directStatement("_x.printStackTrace();");

              } else {
                if(type.isPrimitive() || 
                    type.fullName().equals("java.lang.String") ||
                    type.fullName().equals("java.lang.Double") ||
                    type.fullName().equals("java.lang.Boolean") ||
                    type.fullName().equals("java.lang.Float") ||
                    type.fullName().equals("java.lang.Long") ||
                    type.fullName().equals("java.lang.Integer") ||
                    type.fullName().equals("javax.xml.datatype.XMLGregorianCalendar")) {
                  String appendStr = "\n<" + xmlName + ">";
                  appendBlock.invoke(jStringBuilderVar, "append").arg(appendStr);
                  appendBlock.invoke(jStringBuilderVar, "append").arg(JExpr.ref(varName));
                  appendStr = "</" + xmlName + ">\n";
                  appendBlock.invoke(jStringBuilderVar, "append").arg(appendStr);
                } else {
                  JInvocation toXmlInvoke;
                  toXmlInvoke = JExpr.invoke(JExpr.ref(varName), "toXml");
                  toXmlInvoke.arg(xmlName);
                  appendBlock.invoke(jStringBuilderVar, "append").arg(toXmlInvoke);
                }
              }
              
              try {
              if(type.isPrimitive()) {
                codeBlock.add(appendBlock);
              } else {
                JConditional ifBlock = codeBlock._if(JExpr.ref(varName).ne(JExpr._null()));
                ifBlock._then().add(appendBlock);
              }
              } catch (Exception e) {
                e.printStackTrace();
              }
            }
          } else if (elementInfo.names.size() > 1){
            JType typeIndicator = elementInfo.types.get(0);
            
            Boolean haveSharedTypes = false;
            {
              try {
                Object basis = Utilities.getFieldValue(typeIndicator, "basis");
                Class<?> typeClass = Utilities.getFieldValue(basis, "_class");
                
                if(typeClass.equals(JAXBElement.class)) {
                  haveSharedTypes = true;
                }
              } catch (Exception e) {
                // do nothing
              }
            }
            
            if(haveSharedTypes) {
              // TODO
              codeBlock.directStatement("throw new UnsupportedOperationException(\"contact Lizzie DeYoung at edeyoung@mitre.org to have this functionality implemented\");");
            } else {
              codeBlock.directStatement(" ");
              codeBlock.directStatement("// handeling a choice element");
              JBlock innerCodeBlock = new JBlock();
              for(int i = 0; i < elementInfo.names.size(); i++) {
                String name = elementInfo.names.get(i);
                JType type = elementInfo.types.get(i);
                JExpression condExpr = JExpr.direct(varName + " instanceof " + type.fullName());
                JConditional cond = innerCodeBlock._if(condExpr);
                
                if(type.isPrimitive()) {
                  cond._then().invoke(jStringBuilderVar, "append").arg("< ");
                  cond._then().invoke(jStringBuilderVar, "append").arg(name);
                  cond._then().invoke(jStringBuilderVar, "append").arg(" >");
                  
                  cond._then().invoke(jStringBuilderVar, "append").arg(JExpr.ref(varName));
                  
                  cond._then().invoke(jStringBuilderVar, "append").arg("</ ");
                  cond._then().invoke(jStringBuilderVar, "append").arg(name);
                  cond._then().invoke(jStringBuilderVar, "append").arg(" >");
                } else {
                  JInvocation invoke = cond._then().invoke(jStringBuilderVar, "append");
                  JExpression cast = JExpr.cast(type, JExpr.ref(varName));
                  invoke.arg(cast.invoke("toXml").arg(name));
                }
              }
              codeBlock.add(innerCodeBlock);
            }
          }
        } 
      }
    } else {
      // other
    }
  }

  private boolean addToXml(JDefinedClass clazz) {
    if(clazz.name().equals("TEPMessage")) {
      Boolean breakhere = true;
      breakhere = false;
      if(breakhere) {
        Boolean something = true;
      }  
    }
    
    List<String> elementList = getOrderedElementList(clazz);
    Map<String, APXmlElement> elementInfos = getElementInformation(clazz);
    Map<String, APXmlAttribute> attributeInfos = getAttributeInformation(clazz);

      // create the toXml class
      JCodeModel jCodeModel = new JCodeModel();
      String methodName = "toXml";
      JMethod jmCreate = clazz.method(JMod.PUBLIC, java.lang.String.class, methodName);
      JBlock jBlock = jmCreate.body();

      // the parameter is the name of the element that's being generated
      JVar elementName = jmCreate.param(java.lang.String.class, "elementName");

      // create and instantiate the string builder
      JClass jStringBuilderClass = jCodeModel.ref(StringBuilder.class);
      JVar jStringBuilderVar = jBlock.decl(jStringBuilderClass, "strBuilder");
      jStringBuilderVar.init(JExpr._new(jStringBuilderClass));

      // add the element tag to the string builder
      jBlock.directStatement("// creating element tag");
      jBlock.invoke(jStringBuilderVar, "append").arg("\n< ");
      jBlock.invoke(jStringBuilderVar, "append").arg(elementName);
      // add attribute information
      addAttributeInformation(jBlock, jStringBuilderVar, attributeInfos);
      // end the tag
      jBlock.invoke(jStringBuilderVar, "append").arg(" >");

      // add information for each element
      addElementsInformation(jCodeModel, jBlock, jStringBuilderVar, elementList, elementInfos);

      // add end element tag to the string builder
      jBlock.directStatement(" "); // for some reason an empty string throws an error
      jBlock.directStatement("// creating end element tag");
      jBlock.invoke(jStringBuilderVar, "append").arg("</ ");
      jBlock.invoke(jStringBuilderVar, "append").arg(elementName);
      jBlock.invoke(jStringBuilderVar, "append").arg(" >\n");



      /* Defining some class Variable in method body */
      JClass jClassavpImpl = jCodeModel.ref(java.lang.String.class);
      JVar jvarAvpImpl = jBlock.decl(jClassavpImpl, "xmlString");
      jvarAvpImpl.init(JExpr.invoke(jStringBuilderVar, "toString"));
      /* returning variable */ 
      jBlock._return(jvarAvpImpl);


    return true;
  }
  
  private boolean addSerialization(JDefinedClass clazz) {
    //addFromXml(clazz);
    //clazz._extends(androidPluginClass);
    addToXml(clazz);
    
    return true;
  }
  
  private void clearAllAnnotations(JDefinedClass clazz){
    try {
      // clear class-level annotations
      Field f = JDefinedClass.class.getDeclaredField("annotations");
      f.setAccessible(true);
      f.set(clazz, null);

      // clear field-level annotations
      Map<String, JFieldVar> fields = clazz.fields();
      for (JFieldVar fVar : fields.values()){
        f = JVar.class.getDeclaredField("annotations");
        f.setAccessible(true);
        f.set(fVar, null);
      }
      
      Map<String, JEnumConstant> enums = Utilities.getFieldValue(clazz, "enumConstantsByName");
      if(enums != null && enums.size() > 0) {
        for(JEnumConstant enuum : enums.values()) {
          f = JEnumConstant.class.getDeclaredField("annotations");
          f.setAccessible(true);
          f.set(enuum, null);
        }
      }
    } catch (SecurityException e) {
      e.printStackTrace();
    } catch (NoSuchFieldException e) {
      e.printStackTrace();
    } catch (IllegalArgumentException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
  }

}
